 /*******************************************************************************
  * Copyright (c) 2000, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * Genady Beryozkin, me@genady.org - initial API and implementation
  * IBM Corporation - fixes and cleaning
  *******************************************************************************/
 package org.eclipse.ui.texteditor;

 import java.util.ArrayList ;
 import java.util.List ;
 import java.util.ResourceBundle ;

 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IRewriteTarget;
 import org.eclipse.jface.text.ITextSelection;
 import org.eclipse.jface.text.source.ISourceViewer;

 import org.eclipse.core.runtime.Assert;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.ui.IEditorInput;
 import org.eclipse.ui.IEditorPart;
 import org.eclipse.ui.IEditorReference;
 import org.eclipse.ui.IWorkbenchWindow;
 import org.eclipse.ui.internal.texteditor.CompoundEditExitStrategy;
 import org.eclipse.ui.internal.texteditor.HippieCompletionEngine;
 import org.eclipse.ui.internal.texteditor.ICompoundEditListener;
 import org.eclipse.ui.internal.texteditor.TextEditorPlugin;

 /**
  * This class implements the emacs style completion action. Completion action is
  * a stateful action, as the user may invoke it several times in a row in order
  * to scroll the possible completions.
  *
  * TODO: Sort by editor type
  * TODO: Provide history option
  *
  * @since 3.1
  * @author Genady Beryozkin, me@genady.org
  */
 final class HippieCompleteAction extends TextEditorAction {

     /**
      * This class represents the state of the last completion process. Each time
      * the user moves to a new position and calls this action an instance of
      * this inner class is created and saved in
      * {@link HippieCompleteAction#fLastCompletion}.
      */
     private static class CompletionState {

         /** The length of the last suggestion string */
         int length;

         /** The index of next suggestion (index into the suggestion array) */
         int nextSuggestion;

         /** The caret position at which we insert the suggestions */
         final int startOffset;

         /**
          * The list of suggestions that was computed when the completion action
          * was first invoked
          */
         final String [] suggestions;

         /**
          * Create a new completion state object
          *
          * @param suggestions the array of possible completions
          * @param startOffset the position in the parent document at which the
          * completions will be inserted.
          */
         CompletionState(String [] suggestions, int startOffset) {
             this.suggestions= suggestions;
             this.startOffset= startOffset;
             length= 0;
             nextSuggestion= 0;
         }

         /**
          * Advances the completion state to represent the next completion.
          */
         public void advance() {
             length= suggestions[nextSuggestion].length();
             nextSuggestion= (nextSuggestion + 1) % suggestions.length;
         }
     }

     /**
      * The document that will be manipulated (currently open in the editor)
      */
     private IDocument fDocument;

     /**
      * The completion state that is used to continue the iteration over
      * completion suggestions
      */
     private CompletionState fLastCompletion= null;

     /**
      * The completion engine
      */
     private final HippieCompletionEngine fEngine= new HippieCompletionEngine();

     /** The compound edit exit strategy. */
     private final CompoundEditExitStrategy fExitStrategy= new CompoundEditExitStrategy(ITextEditorActionDefinitionIds.HIPPIE_COMPLETION);
     
     /**
      * Creates a new action.
      *
      * @param bundle the resource bundle
      * @param prefix a prefix to be prepended to the various resource keys
      * (described in <code>ResourceAction</code> constructor), or
      * <code>null</code> if none
      * @param editor the text editor
      */
     HippieCompleteAction(ResourceBundle bundle, String prefix, ITextEditor editor) {
         super(bundle, prefix, editor);
         fExitStrategy.addCompoundListener(new ICompoundEditListener() {
             public void endCompoundEdit() {
                 clearState();
             }
         });
     }

     /**
      * Invalidates the cached completions, removes all registered listeners and
      * sets the cached document to <code>null</code>.
      */
     private void clearState() {
         fLastCompletion= null;

         ITextEditor editor= getTextEditor();
         
         if (editor != null) {
             IRewriteTarget target= (IRewriteTarget) editor.getAdapter(IRewriteTarget.class);
             if (target != null) {
                 fExitStrategy.disarm();
                 target.endCompoundChange();
             }
         }

         fDocument= null;
     }

     /**
      * Perform the next completion.
      */
     private void completeNext() {
         try {
             fDocument.replace(fLastCompletion.startOffset, fLastCompletion.length, fLastCompletion.suggestions[fLastCompletion.nextSuggestion]);
         } catch (BadLocationException e) {
             // we should never get here. different from other places to notify the user.
 log(e);
             clearState();
             return;
         }
         
         // advance the suggestion state
 fLastCompletion.advance();
         
         // move the caret to the insertion point
 ISourceViewer sourceViewer= ((AbstractTextEditor) getTextEditor()).getSourceViewer();
         sourceViewer.setSelectedRange(fLastCompletion.startOffset + fLastCompletion.length, 0);
         sourceViewer.revealRange(fLastCompletion.startOffset, fLastCompletion.length);
         
         fExitStrategy.arm(((AbstractTextEditor) getTextEditor()).getSourceViewer());
     }

     /**
      * Return the list of suggestions from the current document. First the
      * document is searched backwards from the caret position and then forwards.
      *
      * @param prefix the completion prefix
      * @return all possible completions that were found in the current document
      * @throws BadLocationException if accessing the document fails
      */
     private ArrayList createSuggestionsFromOpenDocument(String prefix) throws BadLocationException {
         int selectionOffset= getSelectionOffset();

         ArrayList completions= new ArrayList ();
         completions.addAll(fEngine.getCompletionsBackwards(fDocument, prefix, selectionOffset));
         completions.addAll(fEngine.getCompletionsForward(fDocument, prefix, selectionOffset - prefix.length(), true));

         return completions;
     }

     /**
      * Returns the document currently displayed in the editor, or
      * <code>null</code>
      *
      * @return the document currently displayed in the editor, or
      * <code>null</code>
      */
     private IDocument getCurrentDocument() {
         ITextEditor editor= getTextEditor();
         if (editor == null)
             return null;
         IDocumentProvider provider= editor.getDocumentProvider();
         if (provider == null)
             return null;

         IDocument document= provider.getDocument(editor.getEditorInput());
         return document;
     }

     /**
      * Return the part of a word before the caret. If the caret is not at a
      * middle/end of a word, returns null.
      *
      * @return the prefix at the current cursor position that will be used in
      * the search for possible completions
      * @throws BadLocationException if accessing the document fails
      */
     private String getCurrentPrefix() throws BadLocationException {
         ITextSelection selection= (ITextSelection) getTextEditor().getSelectionProvider().getSelection();
         if (selection.getLength() > 0) {
             return null;
         }
         return fEngine.getPrefixString(fDocument, selection.getOffset());
     }

     /**
      * Returns the current selection (or caret) offset.
      *
      * @return the current selection (or caret) offset
      */
     private int getSelectionOffset() {
         return ((ITextSelection) getTextEditor().getSelectionProvider().getSelection()).getOffset();
     }

     /**
      * Create the array of suggestions. It scans all open text editors and
      * prefers suggestions from the currently open editor. It also adds the
      * empty suggestion at the end.
      *
      * @param prefix the prefix to search for
      * @return the list of all possible suggestions in the currently open
      * editors
      * @throws BadLocationException if accessing the current document fails
      */
     private String [] getSuggestions(String prefix) throws BadLocationException {

         ArrayList suggestions= createSuggestionsFromOpenDocument(prefix);

         IWorkbenchWindow window= getTextEditor().getSite().getWorkbenchWindow();
         IEditorReference editorsArray[]= window.getActivePage().getEditorReferences();

         for (int i= 0; i < editorsArray.length; i++) {
             IEditorPart realEditor= editorsArray[i].getEditor(false);
             if (realEditor instanceof ITextEditor &&
                     !realEditor.equals(getTextEditor())) { // realEditor != null
 ITextEditor textEditor= (ITextEditor)realEditor;
                 IEditorInput input= textEditor.getEditorInput();
                 IDocument doc= textEditor.getDocumentProvider().getDocument(input);

                 suggestions.addAll(fEngine.getCompletionsForward(doc, prefix, 0, false));
             }
         }
         // add the empty suggestion
 suggestions.add(""); //$NON-NLS-1$

         List uniqueSuggestions= fEngine.makeUnique(suggestions);

         return (String []) uniqueSuggestions.toArray(new String [0]);
     }

     /**
      * Returns <code>true</code> if the current completion state is still
      * valid given the current document and selection.
      *
      * @return <code>true</code> if the cached state is valid,
      * <code>false</code> otherwise
      */
     private boolean isStateValid() {
         return fDocument != null
                 && fDocument.equals(getCurrentDocument())
                 && fLastCompletion != null
                 && fLastCompletion.startOffset + fLastCompletion.length == getSelectionOffset();
     }

     /**
      * Notifies the user that there are no suggestions.
      */
     private void notifyUser() {
         // TODO notify via status line?
 getTextEditor().getSite().getShell().getDisplay().beep();
     }

     /*
      * @see org.eclipse.jface.action.Action#run()
      */
     public void run() {
         if (!validateEditorInputState())
             return;

         if (!isStateValid())
             updateState();

         if (isStateValid())
             completeNext();
     }

     /*
      * @see org.eclipse.jface.action.IAction#isEnabled()
      */
     public boolean isEnabled() {
         return canModifyEditor();
     }

     /*
      * @see org.eclipse.ui.texteditor.TextEditorAction#setEditor(org.eclipse.ui.texteditor.ITextEditor)
      */
     public void setEditor(ITextEditor editor) {
         clearState(); // make sure to remove listers before the editor changes!
 super.setEditor(editor);
     }

     /**
      * Update the completion state. The completion cache is updated with the
      * completions based on the currently displayed document and the current
      * selection. To track the validity of the cached state, listeners are
      * registered with the editor and document, and the current document is
      * cached.
      */
     private void updateState() {
         Assert.isNotNull(getTextEditor());

         clearState();

         IDocument document= getCurrentDocument();
         if (document != null) {
             fDocument= document;

             String [] suggestions;
             try {
                 String prefix= getCurrentPrefix();
                 if (prefix == null) {
                     notifyUser();
                     return;
                 }
                 suggestions= getSuggestions(prefix);
             } catch (BadLocationException e) {
                 log(e);
                 return;
             }

             // if it is single empty suggestion
 if (suggestions.length == 1) {
                 notifyUser();
                 return;
             }

             IRewriteTarget target= (IRewriteTarget) getTextEditor().getAdapter(IRewriteTarget.class);
             if (target != null)
                 target.beginCompoundChange();
             
             fLastCompletion= new CompletionState(suggestions, getSelectionOffset());
         }
     }

     /**
      * Logs the exception.
      *
      * @param e the exception
      */
     private void log(BadLocationException e) {
         String msg= e.getLocalizedMessage();
         if (msg == null)
             msg= "unable to access the document"; //$NON-NLS-1$
 TextEditorPlugin.getDefault().getLog().log(new Status(IStatus.ERROR, TextEditorPlugin.PLUGIN_ID, IStatus.OK, msg, e));
     }
 }

